import json, random



# Docker Task
class Task:
    def __init__(self, cpu_demand, memory_demand, blkio_demand, network_demand):
        self.cpu_demand = cpu_demand
        self.memory_demand = memory_demand
        self.blkio_demand = blkio_demand
        self.network_demand = network_demand


    def demand_vector(self):
        return [self.cpu_demand, self.memory_demand, self.blkio_demand, self.network_demand]


    def max_demand(self):
        return max(self.demand_vector())


    def get_task_statistics(self):
        total_statistics = {}
        total_statistics["cpu_demand"] = self.cpu_demand
        total_statistics["memory_demand"] = self.memory_demand
        total_statistics["blkio_demand"] = self.blkio_demand
        total_statistics["network_demand"] = self.network_demand
        return total_statistics


    def __repr__(self):
        return str(self.get_task_statistics())



# Physical Server
class Server:
    def __init__(self, cpu_number, memory_capacity,
                blkio_bandwidth, network_bandwidth,
                cpu_utilization=.0, memory_utilization=.0,
                blkio_utilization=.0, network_utilization=.0):
        self.cpu_number = cpu_number
        self.cpu_utilization = cpu_utilization
        self.cpu_usage = cpu_number * cpu_utilization
        #self.cpu_utilization = [.0 for i in range(self.cpu_number)]

        self.memory_capacity = memory_capacity
        self.memory_utilization = memory_utilization
        self.memory_usage = memory_capacity * memory_utilization

        self.blkio_bandwidth = blkio_bandwidth
        self.blkio_utilization = blkio_utilization
        self.blkio_usage = blkio_bandwidth * blkio_utilization

        self.network_bandwidth = network_bandwidth
        self.network_utilization = network_utilization
        self.network_usage = network_bandwidth * network_utilization


    def check_resource_limit(self, task):
        if (task.cpu_demand + self.cpu_usage > self.cpu_number) \
            or (task.memory_demand + self.memory_usage > self.memory_capacity) \
            or (task.blkio_demand + self.blkio_usage > self.blkio_bandwidth) \
            or (task.network_demand + self.network_usage > self.network_bandwidth):
            return False
        return True


    def add_task(self, task):
        self.cpu_usage += task.cpu_demand
        self.cpu_utilization = self.cpu_usage / self.cpu_number
        # for core in self.cpu_utilization:
        #     if core + task.cpu_demand <= 1:
        #         core += task.cpu_demand
        #         break
        
        self.memory_usage += task.memory_demand
        self.memory_utilization = self.memory_usage / self.memory_capacity

        self.blkio_usage += task.blkio_demand
        self.blkio_utilization = self.blkio_usage / self.blkio_bandwidth

        self.network_usage += task.network_demand
        self.network_utilization = self.network_usage / self.network_bandwidth


    def utilization_vector(self):
        return [self.cpu_utilization, self.memory_utilization, 
                self.blkio_utilization, self.network_utilization]


    def average_utilization(self):
        return (self.cpu_utilization + self.memory_utilization +
                self.blkio_utilization + self.network_utilization) / 4


    def max_utilization(self):
        return max(self.utilization_vector())


    def min_utilization(self):
        return min(self.utilization_vector())


    def balance_ratio(self):
        if self.max_utilization() == 0:
            return 0
        else:
            return self.average_utilization() / self.max_utilization()
    

    def get_server_statistics(self):
        total_statistics = {}
        total_statistics["cpu_statistics"] = {
            "cpu_number":           self.cpu_number,
            "cpu_utilization":      self.cpu_utilization
        }
        total_statistics["memory_statistics"] = {
            "memory_capacity":      self.memory_capacity,
            "memory_utilization":   self.memory_utilization
        }
        total_statistics["blkio_statistics"] = {
            "blkio_bandwidth":      self.blkio_bandwidth,
            "blkio_utilization":    self.blkio_utilization
        }
        total_statistics["network_statistics"] = {
            "network_bandwidth":    self.network_bandwidth,
            "network_utilization":  self.network_utilization
        }
        return total_statistics


    def __repr__(self):
        return str(self.get_server_statistics())



# Server Placement Effect Analyzer
class ServerAnalyzer:   
    def analyze_result(self, servers):
        result = {}
        result["number_of_servers"] = len(servers)
        result["resource_utilization_ratio"] = self.analyze_resource_utilization(servers)
        result["resource_balance_ratio"] = self.analyze_resource_balance(servers)
        return result


    def analyze_resource_utilization(self, servers):
        if len(servers) == 0:
            return 0

        summation = 0
        for s in servers:
            summation += s.average_utilization()
        return summation / len(servers)


    def analyze_resource_balance(self, servers):
        if len(servers) == 0:
            return 0
        
        summation = 0
        for s in servers:
            summation += s.balance_ratio()
        return summation / len(servers)



# Read Data Set
class DataSetReader:
    def read_servers(self, file_name):
        with open(file_name) as f:
            content = json.load(f)

        servers = []
        for s in content:
            new_server = Server(s["cpu_statistics"]["cpu_number"],
                                s["memory_statistics"]["memory_capacity"],
                                s["blkio_statistics"]["blkio_bandwidth"],
                                s["network_statistics"]["network_bandwidth"])
            servers.append(new_server)
        return servers


    def read_tasks(self, file_name):
        with open(file_name) as f:
            content = json.load(f)

        tasks = []
        for t in content:
            new_task = Task(t["cpu_demand"],
                            t["memory_demand"],
                            t["blkio_demand"],
                            t["network_demand"])
            tasks.append(new_task)
        return tasks



# generate data set
class DataSetGenerator:
    def generate_server_data_set(self, server_sample, n):
        return [server_sample.get_server_statistics() for i in range(n)]



    def generate_server_capacity_task_data_set(self, server_sample, n):
        task_sample = Task(server_sample.cpu_number, server_sample.memory_capacity,
                    server_sample.blkio_bandwidth, server_sample.network_bandwidth)
        return [task_sample.get_task_statistics() for i in range(n)]



    def generate_uniform_distribution_task_data_set(self, distribution, n):
        task_data_set = []
        for i in range(n):
            cpu_demand = random.uniform(
                distribution["cpu_lower_bound"], distribution["cpu_upper_bound"])
            memory_demand = random.uniform(
                distribution["memory_lower_bound"], distribution["memory_upper_bound"])
            blkio_demand = random.uniform(
                distribution["blkio_lower_bound"], distribution["blkio_upper_bound"])
            network_demand = random.uniform(
                distribution["network_lower_bound"], distribution["network_upper_bound"])

            new_task = Task(cpu_demand, memory_demand, blkio_demand, network_demand)
            task_data_set.append(new_task.get_task_statistics())
        return task_data_set



    def generate_normal_distribution_task_data_set(self, distribution, n):
        task_data_set = []
        for i in range(n):
            alpha = random.randint(1, 10)
            cpu_demand = alpha * max(0, random.gauss(
                distribution["cpu_average"], distribution["cpu_variance"]))
            memory_demand = alpha * max(0, random.gauss(
                distribution["memory_average"], distribution["memory_variance"]))
            blkio_demand = alpha * max(0, random.gauss(
                distribution["blkio_average"], distribution["blkio_variance"]))
            network_demand = alpha * max(0, random.gauss(
                distribution["network_average"], distribution["network_variance"]))

            new_task = Task(cpu_demand, memory_demand, blkio_demand, network_demand)
            task_data_set.append(new_task.get_task_statistics())
        return task_data_set



# for convenience, create a sample server
def create_sample_server():
    CPU_NUMBER = 1
    MEMORY_CAPACITY = 2
    BLKIO_BANDWIDTH = 20
    NETWORK_BANDWIDTH = 1
    return Server(CPU_NUMBER, MEMORY_CAPACITY, BLKIO_BANDWIDTH, NETWORK_BANDWIDTH)